Skip to content

feat: network transport fallback for isolated server runtimes#384

Open
AlemTuzlak wants to merge 18 commits intomainfrom
worktree-polished-cuddling-lark
Open

feat: network transport fallback for isolated server runtimes#384
AlemTuzlak wants to merge 18 commits intomainfrom
worktree-polished-cuddling-lark

Conversation

@AlemTuzlak
Copy link
Copy Markdown
Collaborator

@AlemTuzlak AlemTuzlak commented Mar 12, 2026

Summary

Fixes TanStack/ai#339

When TanStack Start uses Nitro v3's nitro() Vite plugin (or any runtime that isolates server code in a separate thread/process), devtools events break because globalThis.__TANSTACK_EVENT_TARGET__ is not shared across isolation boundaries.

This PR adds automatic network transport fallback:

  • EventClient detects isolated server environments (no shared EventTarget, no window) and automatically connects to ServerEventBus via WebSocket
  • ServerEventBus distinguishes "server bridge" connections (?bridge=server) from browser clients and routes bridge messages through both emitEventToClients() (browser devtools) and emitToServer() (in-process EventTarget)
  • Echo prevention via 200-entry ring buffer of event IDs
  • Reconnection with exponential backoff (100ms → 5s, max 10 attempts) then HTTP POST fallback
  • Zero API changes — existing consumers work unchanged
  • Zero configuration — detection and fallback are automatic
  • Dev-only — network transport only activates when Vite plugin replaces compile-time placeholders

Changes

  • packages/event-bus/src/server/server.ts — bridge WebSocket support, POST source-based routing
  • packages/event-bus/src/client/client.ts — interface update (eventId, source fields)
  • packages/event-bus-client/src/plugin.ts — network transport detection, WebSocket connection, emit/receive, reconnect, HTTP fallback
  • packages/event-bus-client/src/ring-buffer.ts — new RingBuffer utility for dedup
  • packages/event-bus-client/src/index.ts — export createNetworkTransportClient

Test plan

  • 60 event-bus tests pass (26 server tests including 5 new bridge/POST tests)
  • 28 event-bus-client tests pass (including 4 network transport + 2 integration + 3 ring buffer)
  • Bidirectional events: client→server and server→client via WebSocket
  • Echo deduplication: client does not receive its own events back
  • Event queuing: events queue during connection and flush on connect
  • Multiple isolated clients work simultaneously
  • Manual test with Nitro v3 nitro() plugin in examples/react/start

Summary by CodeRabbit

  • New Features

    • Network transport fallback for devtools in isolated server runtimes (WebSocket + HTTP) with event deduplication and sequencing.
    • New example UI: Server Events panel and server-event hooks for Cloudflare & Nitro demos.
    • Public API: createNetworkTransportClient to opt-in to networked event clients.
  • Documentation

    • Added design and implementation guide for network transport fallback.
  • Tests

    • Extensive unit and integration tests for transport, routing, and ring-buffer behavior.

…er runtimes

Addresses the issue where devtools events are lost when server code runs in
isolated environments (Nitro v3 worker threads, Cloudflare Workers, etc.)
that don't share globalThis with the Vite main thread.
Fix problem description precision, URL matching and handleNewConnection
signature issues, POST handler routing, placeholder convention, triplicate
interface sync, queue preservation, and multi-worker echo safety.
Disambiguate that both standalone and external server POST/upgrade
handlers need updates, and that only WebSocket URL matching needs
prefix change (not SSE/POST URLs).
7-task plan covering: interface updates, ServerEventBus bridge support,
POST handler routing, RingBuffer utility, EventClient network transport
detection, WebSocket connection/emit/receive, and integration tests.
- Accept WebSocket connections with ?bridge=server query parameter
- Track bridge clients separately for proper routing
- Bridge messages route through emit() (broadcast to WS clients + EventTarget)
- Regular browser messages route through emitToServer() (EventTarget only)
- Clean up bridge client tracking on disconnect and stop()
When EventClient detects it is in an isolated server environment
(no shared globalThis.__TANSTACK_EVENT_TARGET__, no window), it
automatically connects to ServerEventBus via WebSocket. Bidirectional:
events emitted in the worker reach the devtools panel, and events
from the devtools panel reach listeners in the worker.

Includes echo prevention via 200-entry ring buffer, exponential
backoff reconnection, HTTP POST fallback, and event queuing.
- Add scheduleReconnect() call in error handler for non-browser runtimes
  where 'close' may not follow 'error'
- Reset wsGaveUp, wsReconnectAttempts, wsReconnectDelay in
  ___destroyNetworkTransport for safe reuse
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 12, 2026

⚠️ No Changeset found

Latest commit: 3f82eeb

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@nx-cloud
Copy link
Copy Markdown

nx-cloud bot commented Mar 12, 2026

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit 64ae1bb

Command Status Duration Result
nx affected --targets=test:eslint,test:sherif,t... ❌ Failed 2m 17s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 22s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-12 14:38:15 UTC

AlemTuzlak and others added 2 commits March 12, 2026 16:59
Two minimal examples for manually testing the network transport fallback:

- examples/react/start-nitro — TanStack Start + Nitro v3 (worker threads)
- examples/react/start-cloudflare — TanStack Start + Cloudflare Workers

Both emit devtools events from server functions and display them in a
custom "Server Events" devtools panel. If events appear in the panel,
the network transport fallback is working correctly.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds a network transport fallback for devtools events: WebSocket bridge with HTTP POST fallback, eventId/source fields on events, a public RingBuffer for deduplication, a createNetworkTransportClient API, server bridge routing, compile-time placeholders, and extensive tests and example apps (Cloudflare, Nitro).

Changes

Cohort / File(s) Summary
Design & Documentation
docs/superpowers/plans/2026-03-12-network-transport-fallback.md, docs/superpowers/specs/2026-03-12-network-transport-fallback-design.md
Design and implementation plan describing network transport fallback, detection, protocol changes, error modes, and test checklist.
Event Protocol & Server Core
packages/event-bus/src/server/server.ts, packages/event-bus/src/client/client.ts
Extended TanStackDevtoolsEvent with eventId?: string and source?: 'server-bridge'; ServerEventBus tracks bridge clients, distinguishes bridge vs browser routing, and adjusts WS upgrade/POST handling.
Event Client & Network Transport
packages/event-bus-client/src/plugin.ts, packages/event-bus-client/src/index.ts
Added network transport plumbing to EventClient: WebSocket connect, backoff/reconnect, HTTP POST fallback, event queuing/flush, echo-dedup logic, internal enable/destroy hooks, and exported createNetworkTransportClient.
RingBuffer Utility
packages/event-bus-client/src/ring-buffer.ts, packages/event-bus-client/tests/ring-buffer.test.ts
New exported RingBuffer class for fixed-capacity deduplication tracking with unit tests covering add/has/eviction/wrap-around.
Server & Network Tests
packages/event-bus-client/tests/network-transport.test.ts, packages/event-bus-client/tests/integration.test.ts, packages/event-bus/tests/server.test.ts
Integration and unit tests for WS lifecycle, POST routing, bridge acceptance, broadcast/routing semantics, queuing/flush behavior, echo prevention, and multi-client scenarios.
Examples — Cloudflare
examples/react/start-cloudflare/* (e.g., .gitignore, package.json, tsconfig.json, vite.config.ts, wrangler.jsonc, src/devtools/*, src/routes/*, src/router.tsx)
New Cloudflare Workers example app demonstrating server-event emission, ServerEventsPanel UI, server-event client, router, and Vite setup for devtools network transport.
Examples — Nitro
examples/react/start-nitro/* (e.g., .gitignore, package.json, tsconfig.json, vite.config.ts, src/devtools/*, src/routes/*, src/router.tsx)
New Nitro v3 example app mirroring Cloudflare example: server functions, ServerEventsPanel, server-event client, router, and Vite config with devtools connection.
Exports & Indexing
packages/event-bus-client/src/index.ts
Re-exported createNetworkTransportClient alongside EventClient.
Misc — Project
.gitignore, package.json, various example config files
Minor repo-level ignores and size-limit tweak; many added example configs and manifests.

Sequence Diagram

sequenceDiagram
    participant Client as Isolated Client\n(EventClient)
    participant WS as WebSocket\nConnection
    participant HTTP as HTTP Fallback\nPOST Endpoint
    participant Server as ServerEventBus\n(Main Process)

    Note over Client,Server: Init and Detection
    Client->>Client: detect network transport / createNetworkTransportClient
    Client->>WS: connectWebSocket() to /__devtools/ws?bridge=server
    alt WebSocket Available
        WS->>Server: Upgrade request (bridge=server)
        Server->>Server: register bridge client
    else WebSocket Unavailable
        Client->>Client: scheduleReconnect() / backoff
    end

    Note over Client,Server: Emit Event
    Client->>Client: generate eventId, add to RingBuffer
    alt WS Connected
        Client->>WS: send { event, eventId, source:'server-bridge' }
        WS->>Server: receive and route to in-process & browsers
    else WS Disconnected
        Client->>Client: queue event
        Client->>HTTP: POST /__devtools/send { event, eventId, source:'server-bridge' }
        HTTP->>Server: receive and route
    end

    Note over Server,Client: Server → Client Delivery
    Server->>WS: send event to bridge client
    WS->>Client: deliver event
    Client->>Client: if RingBuffer.has(eventId) then discard else emit to listeners

    Note over Client,Server: Reconnect & Flush
    Client->>Client: on reconnect flush queued events (new eventIds)
    Client->>WS: send buffered events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A tiny hop, a network trail,
Through WS winds and HTTP gale,
RingBuffer hums, IDs in tow,
Events flow where they need to go,
Examples spring — no runtime left to wail.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly and specifically describes the main change: adding network transport fallback for isolated server runtimes.
Description check ✅ Passed The description is comprehensive, covering summary, changes, test plan, and context. It includes required checklist items and explains the motivation.
Linked Issues check ✅ Passed The PR fully addresses objectives from issue #339 by implementing network transport fallback via WebSocket/HTTP to handle isolated server runtimes.
Out of Scope Changes check ✅ Passed All changes are in scope: network transport infrastructure (WebSocket, reconnect, RingBuffer, HTTP fallback), example applications demonstrating it, and comprehensive tests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-polished-cuddling-lark
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch worktree-polished-cuddling-lark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (8)
examples/react/start-cloudflare/.gitignore (1)

6-11: Consider adding env variant ignores to reduce secret leak risk.

You already ignore .env; adding .env.* and Cloudflare local secret files (for example .dev.vars / .dev.vars.*) would provide better protection against accidental commits in local/dev workflows.

Proposed update
 .env
+.env.*
+.dev.vars
+.dev.vars.*
 .nitro
 .tanstack
 .wrangler
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/.gitignore` around lines 6 - 11, Update the
.gitignore entry that currently lists ".env" to also ignore environment file
variants and common Cloudflare local secret files: add patterns like ".env.*",
"*.env.local", ".dev.vars" and ".dev.vars.*" (or other local secret naming used
in this project) so accidental commits of local/dev secrets are prevented;
ensure these patterns are added alongside existing entries (".env", ".nitro",
".tanstack", ".wrangler", ".output", ".vinxi") in the
examples/react/start-cloudflare/.gitignore file.
examples/react/start-cloudflare/package.json (1)

12-20: Consider moving build-time Vite plugins to devDependencies.

The following packages are Vite plugins used only during development/build and do not run at runtime in Cloudflare Workers:

  • @cloudflare/vite-plugin (line 12)
  • @tanstack/router-plugin (line 17)
  • vite-tsconfig-paths (line 20)

Moving them to devDependencies more accurately reflects their purpose and can reduce confusion about what runs in the deployed worker.

♻️ Proposed fix
   "dependencies": {
-    "@cloudflare/vite-plugin": "^1.13.8",
     "@tanstack/devtools-event-client": "workspace:*",
     "@tanstack/react-devtools": "workspace:*",
     "@tanstack/react-router": "^1.132.0",
     "@tanstack/react-start": "^1.132.0",
-    "@tanstack/router-plugin": "^1.132.0",
     "react": "^19.2.0",
-    "react-dom": "^19.2.0",
-    "vite-tsconfig-paths": "^6.0.2"
+    "react-dom": "^19.2.0"
   },
   "devDependencies": {
+    "@cloudflare/vite-plugin": "^1.13.8",
     "@tanstack/devtools-vite": "workspace:*",
+    "@tanstack/router-plugin": "^1.132.0",
     "@types/node": "^22.15.2",
     "@types/react": "^19.2.0",
     "@types/react-dom": "^19.2.0",
     "@vitejs/plugin-react": "^5.0.4",
     "typescript": "~5.9.2",
     "vite": "^7.1.7",
+    "vite-tsconfig-paths": "^6.0.2",
     "wrangler": "^4.40.3"
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/package.json` around lines 12 - 20, The
package.json currently lists build-time Vite plugins as runtime dependencies;
remove "@cloudflare/vite-plugin", "@tanstack/router-plugin", and
"vite-tsconfig-paths" from the "dependencies" section and add them to
"devDependencies" with the same version strings so they are only installed for
build/dev. Ensure you update the package.json keys accordingly (keep other
packages untouched) and run your package manager to refresh lockfiles so the
change is reflected in installs.
examples/react/start-nitro/package.json (1)

16-16: Pin nitro to a specific version instead of using latest.

At Line 16, "nitro": "latest" is inconsistent with the rest of the dependencies and makes this example non-reproducible. The lockfile resolves to nitro@3.0.1-alpha.2; pin the dependency to this version or a specific stable release:

-    "nitro": "latest",
+    "nitro": "^3.0.1-alpha.2",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/package.json` at line 16, The dependency entry
"nitro": "latest" in package.json makes the example non-reproducible; update the
"nitro" dependency (the "nitro" property in package.json) to a specific version
(e.g., "3.0.1-alpha.2" or a chosen stable release) so the example and lockfile
remain deterministic, then run npm/yarn install to refresh the lockfile.
examples/react/start-cloudflare/src/routes/index.tsx (1)

60-63: Consider adding error handling for server function calls.

The async button handlers call server functions without try/catch. If a server function fails (network error, worker crash), the promise rejection will be unhandled.

💡 Example error handling
         onClick={async () => {
+          try {
             const msg = await greet()
             addResult(msg)
+          } catch (e) {
+            addResult(`Error: ${e instanceof Error ? e.message : 'Unknown error'}`)
+          }
         }}

Also applies to: 78-81, 96-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/src/routes/index.tsx` around lines 60 - 63,
Wrap each async server-call handler (the onClick callbacks that call greet(),
and similar handlers at the other spots) in a try/catch: call the server
function inside try, await the result, and then call addResult(msg) on success;
in catch, log the error and surface a user-friendly message (e.g., via addResult
or an error state) so promise rejections from greet() (or other server
functions) are handled and do not stay unhandled. Ensure you update all three
handlers (the one invoking greet() and the other handlers at the 78-81 and 96-99
locations) and reference the existing functions greet() and addResult when
implementing the try/catch handling.
examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx (1)

105-107: Consider a more unique key for event list items.

Using ${ev.timestamp}-${index} as the key could cause React reconciliation issues if events arrive with identical timestamps (possible with rapid emission). Since events are prepended, the index of existing items shifts on each new event, which combined with timestamp collisions could lead to incorrect DOM updates.

💡 Consider adding a unique identifier

If events gain an eventId field from the transport layer (as mentioned in the PR objectives), use that instead:

-            <div
-              key={`${ev.timestamp}-${index}`}
+            <div
+              key={ev.eventId ?? `${ev.timestamp}-${index}`}

Alternatively, generate a client-side ID when storing events.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx` around
lines 105 - 107, The list key for events in ServerEventsPanel (events.map ->
key={`${ev.timestamp}-${index}`}) is unstable; change it to use a stable unique
id: prefer using ev.eventId (if provided by the transport) or a client-generated
id assigned when the event is created/added (e.g., set ev.id or event.id inside
the event insertion logic). Update the mapping to use that stable id
(key={ev.eventId || ev.id}) so prepending new events won't shift keys and break
React reconciliation.
packages/event-bus/src/server/server.ts (1)

156-167: Extract the POST bridge routing into one helper.

Both server modes now duplicate the same source === 'server-bridge' branch. Pulling that into a shared helper will keep standalone and piggyback mode from drifting the next time the bridge protocol changes.

Also applies to: 274-288

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus/src/server/server.ts` around lines 156 - 167, Extract the
duplicated POST bridge routing into a single helper (e.g.
handleDevtoolsSendRequest) that performs the body collection, calls
parseWithBigInt, logs via this.debugLog('Received event from client', msg) and
then calls either this.emit(msg) or this.emitToServer(msg) based on msg.source
=== 'server-bridge'; replace the duplicated blocks currently handling req.url
=== '/__devtools/send' in both server modes (the block around the
parseWithBigInt/this.debugLog/this.emit/this.emitToServer sequence and the
similar block at the other location) with calls to this new helper so both modes
share the same logic.
packages/event-bus-client/src/plugin.ts (1)

581-595: Use plain comments for these private helpers.

@internal JSDoc on private methods will still flow into generated docs, which makes the website output noisy for implementation details. A normal // comment here is a better fit.

As per coding guidelines, **/*.{js,ts,jsx,tsx}: JSDoc should read like documentation as it gets converted to markdown docs for the website.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 581 - 595, The JSDoc
`@internal` on private helper methods is leaking into generated docs; replace the
/** `@internal` */ JSDoc blocks on the private helpers (specifically
___enableNetworkTransport and ___destroyNetworkTransport) with plain single-line
comments (e.g., // internal — only for testing) so they are treated as
implementation comments rather than documentation and won’t be emitted into the
site output.
examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx (1)

20-28: Consider hydration safety for time formatting.

toLocaleTimeString can produce different output on server vs client due to locale/timezone differences, potentially causing hydration mismatches. Since this is a devtools panel (client-only rendering context), this is likely fine, but worth noting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx` around lines
20 - 28, formatTime currently uses toLocaleTimeString which can differ between
server and client and cause hydration mismatches; make the output deterministic
by specifying an explicit timeZone (e.g., add timeZone: 'UTC' to the options) or
move formatting to run only on the client (compute inside a useEffect or when
rendering client-only components) so timezone/locale differences won't cause
hydration errors — update the formatTime function to use Intl.DateTimeFormat or
toLocaleTimeString with a fixed timeZone option, or ensure calls to formatTime
occur only on the client side.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/react/start-nitro/package.json`:
- Line 19: Move the "vite-tsconfig-paths" dependency from runtime dependencies
into devDependencies: remove the "vite-tsconfig-paths": "^6.0.2" entry under
dependencies in package.json and add the same entry under devDependencies (so
build-time only tools used by vite.config.ts are not bundled at runtime); ensure
package-lock/yarn lock is updated by reinstalling or running the package manager
after the change.

In `@packages/event-bus-client/src/plugin.ts`:
- Around line 50-54: The event ID generation (generateEventId using
globalEventIdCounter) is only unique per module instance and can collide across
workers; create a process-scoped random/unique prefix (e.g., instancePrefix
generated once at module init using crypto.randomBytes or a high-entropy
Math.random hex) and prepend it to the existing
`${++globalEventIdCounter}-${Date.now()}` scheme so generateEventId returns
`${instancePrefix}-${++globalEventIdCounter}-${Date.now()}`, ensuring
instance-scoped entropy and avoiding cross-worker collisions; ensure
instancePrefix is initialized once (top-level) and referenced in
generateEventId.
- Around line 475-483: The branch inside the this.#useNetworkTransport path
incorrectly queues events when the client has given up on WebSocket; update the
logic in the block that calls createEventPayload so that if this.#wsGaveUp is
true you bypass queuing and connectWebSocket(), and immediately call
sendViaNetwork (or sendViaHttp for non-network transport) instead; specifically,
in the code handling this.#useNetworkTransport, check this.#wsGaveUp before
pushing to this.#queuedEvents and short-circuit to sendViaNetwork/sendViaHttp,
leaving connectWebSocket() only for cases where a reconnect attempt should
actually be made.
- Around line 304-308: The bridge currently uses JSON.parse/JSON.stringify which
drops BigInt values; change the ws message handler in
ws.addEventListener('message') to call parseWithBigInt(data) instead of
JSON.parse(data), and update the bridge send methods (the functions around the
send calls at the locations corresponding to lines 408 and 428) to use
stringifyWithBigInt(payload) instead of JSON.stringify(payload). Ensure
parseWithBigInt and stringifyWithBigInt are imported from the event-bus
utilities and replace all plain JSON.parse/JSON.stringify usages in those
handlers/sendters to maintain BigInt-safe wire format compatibility.

In `@packages/event-bus-client/src/ring-buffer.ts`:
- Around line 7-11: The RingBuffer constructor must validate the incoming
capacity and fail fast for non-positive or non-integer values: in the
constructor (class RingBuffer) add a guard that checks capacity is a positive
integer (capacity > 0 && Number.isInteger(capacity)) and throw a clear
RangeError or TypeError if not, so fields like `#capacity`, `#buffer` and `#index`
remain valid and add() cannot enter a broken state; update the constructor that
currently sets this.#capacity, this.#buffer and this.#set to perform this
validation before initializing those fields.

---

Nitpick comments:
In `@examples/react/start-cloudflare/.gitignore`:
- Around line 6-11: Update the .gitignore entry that currently lists ".env" to
also ignore environment file variants and common Cloudflare local secret files:
add patterns like ".env.*", "*.env.local", ".dev.vars" and ".dev.vars.*" (or
other local secret naming used in this project) so accidental commits of
local/dev secrets are prevented; ensure these patterns are added alongside
existing entries (".env", ".nitro", ".tanstack", ".wrangler", ".output",
".vinxi") in the examples/react/start-cloudflare/.gitignore file.

In `@examples/react/start-cloudflare/package.json`:
- Around line 12-20: The package.json currently lists build-time Vite plugins as
runtime dependencies; remove "@cloudflare/vite-plugin",
"@tanstack/router-plugin", and "vite-tsconfig-paths" from the "dependencies"
section and add them to "devDependencies" with the same version strings so they
are only installed for build/dev. Ensure you update the package.json keys
accordingly (keep other packages untouched) and run your package manager to
refresh lockfiles so the change is reflected in installs.

In `@examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx`:
- Around line 105-107: The list key for events in ServerEventsPanel (events.map
-> key={`${ev.timestamp}-${index}`}) is unstable; change it to use a stable
unique id: prefer using ev.eventId (if provided by the transport) or a
client-generated id assigned when the event is created/added (e.g., set ev.id or
event.id inside the event insertion logic). Update the mapping to use that
stable id (key={ev.eventId || ev.id}) so prepending new events won't shift keys
and break React reconciliation.

In `@examples/react/start-cloudflare/src/routes/index.tsx`:
- Around line 60-63: Wrap each async server-call handler (the onClick callbacks
that call greet(), and similar handlers at the other spots) in a try/catch: call
the server function inside try, await the result, and then call addResult(msg)
on success; in catch, log the error and surface a user-friendly message (e.g.,
via addResult or an error state) so promise rejections from greet() (or other
server functions) are handled and do not stay unhandled. Ensure you update all
three handlers (the one invoking greet() and the other handlers at the 78-81 and
96-99 locations) and reference the existing functions greet() and addResult when
implementing the try/catch handling.

In `@examples/react/start-nitro/package.json`:
- Line 16: The dependency entry "nitro": "latest" in package.json makes the
example non-reproducible; update the "nitro" dependency (the "nitro" property in
package.json) to a specific version (e.g., "3.0.1-alpha.2" or a chosen stable
release) so the example and lockfile remain deterministic, then run npm/yarn
install to refresh the lockfile.

In `@examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx`:
- Around line 20-28: formatTime currently uses toLocaleTimeString which can
differ between server and client and cause hydration mismatches; make the output
deterministic by specifying an explicit timeZone (e.g., add timeZone: 'UTC' to
the options) or move formatting to run only on the client (compute inside a
useEffect or when rendering client-only components) so timezone/locale
differences won't cause hydration errors — update the formatTime function to use
Intl.DateTimeFormat or toLocaleTimeString with a fixed timeZone option, or
ensure calls to formatTime occur only on the client side.

In `@packages/event-bus-client/src/plugin.ts`:
- Around line 581-595: The JSDoc `@internal` on private helper methods is leaking
into generated docs; replace the /** `@internal` */ JSDoc blocks on the private
helpers (specifically ___enableNetworkTransport and ___destroyNetworkTransport)
with plain single-line comments (e.g., // internal — only for testing) so they
are treated as implementation comments rather than documentation and won’t be
emitted into the site output.

In `@packages/event-bus/src/server/server.ts`:
- Around line 156-167: Extract the duplicated POST bridge routing into a single
helper (e.g. handleDevtoolsSendRequest) that performs the body collection, calls
parseWithBigInt, logs via this.debugLog('Received event from client', msg) and
then calls either this.emit(msg) or this.emitToServer(msg) based on msg.source
=== 'server-bridge'; replace the duplicated blocks currently handling req.url
=== '/__devtools/send' in both server modes (the block around the
parseWithBigInt/this.debugLog/this.emit/this.emitToServer sequence and the
similar block at the other location) with calls to this new helper so both modes
share the same logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 23c57afd-2ae3-40ad-89fd-677242f015bd

📥 Commits

Reviewing files that changed from the base of the PR and between 1fc0afa and 3f82eeb.

📒 Files selected for processing (32)
  • docs/superpowers/plans/2026-03-12-network-transport-fallback.md
  • docs/superpowers/specs/2026-03-12-network-transport-fallback-design.md
  • examples/react/start-cloudflare/.gitignore
  • examples/react/start-cloudflare/package.json
  • examples/react/start-cloudflare/src/devtools/ServerEventsPanel.tsx
  • examples/react/start-cloudflare/src/devtools/index.ts
  • examples/react/start-cloudflare/src/devtools/server-event-client.ts
  • examples/react/start-cloudflare/src/router.tsx
  • examples/react/start-cloudflare/src/routes/__root.tsx
  • examples/react/start-cloudflare/src/routes/index.tsx
  • examples/react/start-cloudflare/tsconfig.json
  • examples/react/start-cloudflare/vite.config.ts
  • examples/react/start-cloudflare/wrangler.jsonc
  • examples/react/start-nitro/.gitignore
  • examples/react/start-nitro/package.json
  • examples/react/start-nitro/src/devtools/ServerEventsPanel.tsx
  • examples/react/start-nitro/src/devtools/index.ts
  • examples/react/start-nitro/src/devtools/server-event-client.ts
  • examples/react/start-nitro/src/router.tsx
  • examples/react/start-nitro/src/routes/__root.tsx
  • examples/react/start-nitro/src/routes/index.tsx
  • examples/react/start-nitro/tsconfig.json
  • examples/react/start-nitro/vite.config.ts
  • packages/event-bus-client/src/index.ts
  • packages/event-bus-client/src/plugin.ts
  • packages/event-bus-client/src/ring-buffer.ts
  • packages/event-bus-client/tests/integration.test.ts
  • packages/event-bus-client/tests/network-transport.test.ts
  • packages/event-bus-client/tests/ring-buffer.test.ts
  • packages/event-bus/src/client/client.ts
  • packages/event-bus/src/server/server.ts
  • packages/event-bus/tests/server.test.ts

"nitro": "latest",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"vite-tsconfig-paths": "^6.0.2"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Search for vite-tsconfig-paths usage:"
rg -n 'vite-tsconfig-paths|tsconfigPaths\s*\(' --glob '**/*.{ts,tsx,js,jsx,mjs,cjs,mts,cts}'

Repository: TanStack/devtools

Length of output: 484


🏁 Script executed:

cat examples/react/start-nitro/package.json

Repository: TanStack/devtools

Length of output: 860


Move vite-tsconfig-paths to devDependencies.

This package is imported only in vite.config.ts (build-time configuration). It should not be in dependencies since it's not used at runtime.

Proposed change
   "dependencies": {
     "@tanstack/devtools-event-client": "workspace:*",
     "@tanstack/react-devtools": "workspace:*",
     "@tanstack/react-router": "^1.132.0",
     "@tanstack/react-start": "^1.132.0",
     "@tanstack/router-plugin": "^1.132.0",
     "nitro": "latest",
     "react": "^19.2.0",
     "react-dom": "^19.2.0",
-    "vite-tsconfig-paths": "^6.0.2"
   },
   "devDependencies": {
     "@tanstack/devtools-vite": "workspace:*",
     "@types/node": "^22.15.2",
     "@types/react": "^19.2.0",
     "@types/react-dom": "^19.2.0",
     "@vitejs/plugin-react": "^5.0.4",
+    "vite-tsconfig-paths": "^6.0.2",
     "typescript": "~5.9.2",
     "vite": "^7.1.7"
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"vite-tsconfig-paths": "^6.0.2"
"dependencies": {
"@tanstack/devtools-event-client": "workspace:*",
"@tanstack/react-devtools": "workspace:*",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"nitro": "latest",
"react": "^19.2.0",
"react-dom": "^19.2.0"
},
"devDependencies": {
"@tanstack/devtools-vite": "workspace:*",
"@types/node": "^22.15.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"vite-tsconfig-paths": "^6.0.2",
"typescript": "~5.9.2",
"vite": "^7.1.7"
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/package.json` at line 19, Move the
"vite-tsconfig-paths" dependency from runtime dependencies into devDependencies:
remove the "vite-tsconfig-paths": "^6.0.2" entry under dependencies in
package.json and add the same entry under devDependencies (so build-time only
tools used by vite.config.ts are not bundled at runtime); ensure
package-lock/yarn lock is updated by reinstalling or running the package manager
after the change.

Comment on lines +50 to +54
let globalEventIdCounter = 0

function generateEventId(): string {
return `${++globalEventIdCounter}-${Date.now()}`
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make eventId unique across worker instances.

++counter + Date.now() is only unique inside one module instance. Two isolated workers can both generate the same first IDs in the same millisecond, and then falsely drop each other's events as echoes. Add a per-process random prefix or similar instance-scoped entropy.

🔧 One low-dependency fix
 let globalEventIdCounter = 0
+const eventIdPrefix =
+  `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`

 function generateEventId(): string {
-  return `${++globalEventIdCounter}-${Date.now()}`
+  return `${eventIdPrefix}-${++globalEventIdCounter}`
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let globalEventIdCounter = 0
function generateEventId(): string {
return `${++globalEventIdCounter}-${Date.now()}`
}
let globalEventIdCounter = 0
const eventIdPrefix =
`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`
function generateEventId(): string {
return `${eventIdPrefix}-${++globalEventIdCounter}`
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 50 - 54, The event ID
generation (generateEventId using globalEventIdCounter) is only unique per
module instance and can collide across workers; create a process-scoped
random/unique prefix (e.g., instancePrefix generated once at module init using
crypto.randomBytes or a high-entropy Math.random hex) and prepend it to the
existing `${++globalEventIdCounter}-${Date.now()}` scheme so generateEventId
returns `${instancePrefix}-${++globalEventIdCounter}-${Date.now()}`, ensuring
instance-scoped entropy and avoiding cross-worker collisions; ensure
instancePrefix is initialized once (top-level) and referenced in
generateEventId.

Comment on lines +304 to +308
ws.addEventListener('message', (e) => {
try {
const data = typeof e.data === 'string' ? e.data : e.data.toString()
const event = JSON.parse(data)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Compare serializer usage in the bridge and core event-bus transport code.
fd 'plugin\.ts$|client\.ts$|server\.ts$' packages/event-bus packages/event-bus-client -t f \
  | xargs rg -n 'parseWithBigInt|stringifyWithBigInt|JSON\.parse|JSON\.stringify'

Repository: TanStack/devtools

Length of output: 1336


🏁 Script executed:

cat -n packages/event-bus-client/src/plugin.ts | sed -n '300,315p'

Repository: TanStack/devtools

Length of output: 703


🏁 Script executed:

cat -n packages/event-bus-client/src/plugin.ts | sed -n '385,435p'

Repository: TanStack/devtools

Length of output: 2030


🏁 Script executed:

head -20 packages/event-bus-client/src/plugin.ts | cat -n

Repository: TanStack/devtools

Length of output: 983


🏁 Script executed:

ls -la packages/event-bus-client/src/ | head -20

Repository: TanStack/devtools

Length of output: 358


🏁 Script executed:

fd -t f 'json\.' packages/event-bus-client packages/event-bus

Repository: TanStack/devtools

Length of output: 139


🏁 Script executed:

cat packages/event-bus/src/utils/json.ts

Repository: TanStack/devtools

Length of output: 855


Use parseWithBigInt and stringifyWithBigInt for bridge serialization to match the rest of the event-bus.

The server and browser bus use parseWithBigInt / stringifyWithBigInt for transport serialization. The bridge currently uses plain JSON.parse and JSON.stringify, which silently loses BigInt values and breaks wire format compatibility. Update line 307 (message handler) and lines 408, 428 (send methods) to use the BigInt-safe serializers imported from the event-bus utilities.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 304 - 308, The bridge
currently uses JSON.parse/JSON.stringify which drops BigInt values; change the
ws message handler in ws.addEventListener('message') to call
parseWithBigInt(data) instead of JSON.parse(data), and update the bridge send
methods (the functions around the send calls at the locations corresponding to
lines 408 and 428) to use stringifyWithBigInt(payload) instead of
JSON.stringify(payload). Ensure parseWithBigInt and stringifyWithBigInt are
imported from the event-bus utilities and replace all plain
JSON.parse/JSON.stringify usages in those handlers/sendters to maintain
BigInt-safe wire format compatibility.

Comment on lines +475 to +483
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

HTTP-only mode never sends later emits.

After scheduleReconnect() sets #wsGaveUp = true, this branch still queues on !#connected and connectWebSocket() immediately returns, so every subsequent event stays in #queuedEvents forever. Short-circuit to sendViaNetwork() / sendViaHttp() once the client has already given up on WebSocket.

💡 Minimal fix
     if (this.#useNetworkTransport) {
       const event = this.createEventPayload(eventSuffix, payload)
+      if (this.#wsGaveUp) {
+        this.sendViaNetwork(event)
+        return
+      }
       if (!this.#connected) {
         this.#queuedEvents.push(event)
         this.connectWebSocket()
         return
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
if (this.#useNetworkTransport) {
const event = this.createEventPayload(eventSuffix, payload)
if (this.#wsGaveUp) {
this.sendViaNetwork(event)
return
}
if (!this.#connected) {
this.#queuedEvents.push(event)
this.connectWebSocket()
return
}
this.sendViaNetwork(event)
return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/plugin.ts` around lines 475 - 483, The branch
inside the this.#useNetworkTransport path incorrectly queues events when the
client has given up on WebSocket; update the logic in the block that calls
createEventPayload so that if this.#wsGaveUp is true you bypass queuing and
connectWebSocket(), and immediately call sendViaNetwork (or sendViaHttp for
non-network transport) instead; specifically, in the code handling
this.#useNetworkTransport, check this.#wsGaveUp before pushing to
this.#queuedEvents and short-circuit to sendViaNetwork/sendViaHttp, leaving
connectWebSocket() only for cases where a reconnect attempt should actually be
made.

Comment on lines +7 to +11
constructor(capacity: number) {
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Reject invalid capacities in the constructor.

capacity <= 0 leaves add() in a broken state (#index becomes NaN) and negative values surface a generic Array() error. Since RingBuffer is exported, fail fast with a clear positive-integer check.

🛡️ Proposed guard
  constructor(capacity: number) {
+   if (!Number.isInteger(capacity) || capacity <= 0) {
+     throw new RangeError('RingBuffer capacity must be a positive integer')
+   }
    this.#capacity = capacity
    this.#buffer = new Array(capacity).fill('')
    this.#set = new Set()
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(capacity: number) {
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
constructor(capacity: number) {
if (!Number.isInteger(capacity) || capacity <= 0) {
throw new RangeError('RingBuffer capacity must be a positive integer')
}
this.#capacity = capacity
this.#buffer = new Array(capacity).fill('')
this.#set = new Set()
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/event-bus-client/src/ring-buffer.ts` around lines 7 - 11, The
RingBuffer constructor must validate the incoming capacity and fail fast for
non-positive or non-integer values: in the constructor (class RingBuffer) add a
guard that checks capacity is a positive integer (capacity > 0 &&
Number.isInteger(capacity)) and throw a clear RangeError or TypeError if not, so
fields like `#capacity`, `#buffer` and `#index` remain valid and add() cannot enter a
broken state; update the constructor that currently sets this.#capacity,
this.#buffer and this.#set to perform this validation before initializing those
fields.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
examples/react/start-nitro/src/routes/__root.tsx (1)

24-37: Gate devtools mounting to development mode.

connectToServerBus: true triggers network connection attempts when mounted; wrapping this in a dev-only guard avoids unnecessary production runtime overhead in this example.

Suggested change
-        <TanStackDevtools
-        eventBusConfig={{
-          connectToServerBus: true
-        }}
-          config={{ position: 'bottom-right' }}
-          plugins={[
-            {
-              id: 'server-events',
-              name: 'Server Events',
-              render: <ServerEventsPanel />,
-            },
-          ]}
-        />
+        {import.meta.env.DEV ? (
+          <TanStackDevtools
+            eventBusConfig={{
+              connectToServerBus: true,
+            }}
+            config={{ position: 'bottom-right' }}
+            plugins={[
+              {
+                id: 'server-events',
+                name: 'Server Events',
+                render: <ServerEventsPanel />,
+              },
+            ]}
+          />
+        ) : null}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start-nitro/src/routes/__root.tsx` around lines 24 - 37, The
TanStackDevtools block mounts a network-connected event bus because
eventBusConfig.connectToServerBus is true; guard its mounting to development
only by conditionally rendering TanStackDevtools (or setting connectToServerBus:
false in production) — wrap the TanStackDevtools usage that includes
eventBusConfig and the ServerEventsPanel plugin in a dev-only check (e.g.,
process.env.NODE_ENV === 'development' or import.meta.env.DEV) so that
ServerEventsPanel and connectToServerBus are not active in production.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@examples/react/start-nitro/src/routes/__root.tsx`:
- Around line 24-37: The TanStackDevtools block mounts a network-connected event
bus because eventBusConfig.connectToServerBus is true; guard its mounting to
development only by conditionally rendering TanStackDevtools (or setting
connectToServerBus: false in production) — wrap the TanStackDevtools usage that
includes eventBusConfig and the ServerEventsPanel plugin in a dev-only check
(e.g., process.env.NODE_ENV === 'development' or import.meta.env.DEV) so that
ServerEventsPanel and connectToServerBus are not active in production.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9b89ae07-b4c2-4ac3-b75f-0273477dc2f3

📥 Commits

Reviewing files that changed from the base of the PR and between 3f82eeb and 939ac70.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (4)
  • .gitignore
  • examples/react/start-cloudflare/src/routes/__root.tsx
  • examples/react/start-nitro/src/routes/__root.tsx
  • package.json
✅ Files skipped from review due to trivial changes (2)
  • .gitignore
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/react/start-cloudflare/src/routes/__root.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: AI Devtools ignores client-sourced assistant messages and streaming chunks

1 participant